<?php
// @formatter:off
/**
 * @file WCDBIndex.php
 * @author Alejandro Dario Simi
 * @date $Date: 2013-08-05 00:26:28 +0000 (Mon, 05 Aug 2013) $
 *
 * $Id: WCDBIndex.php 100 2013-08-05 00:26:28Z daemonraco@gmail.com $
 * $URL: http://wcomix.googlecode.com/svn/tags/wcomix-1.0.0.2/includes/db/WCDBIndex.php $
 */
// @formatter:on

require_once "{$wcPaths["includes-directory"]}/db/WCDBItem.abs.php";

/**
 * @class WCDBIndex
 * This class represents a database index, it hold the logic needed to check its
 * structure against and XML specification file and to give a way to recover it.
 *
 * @warning: this is valid only for MySQL databases.
 */
class WCDBIndex extends WCDBItem {
	/**
	 * @var boolean This flag indicates if there is a difference between XML
	 * and database specification about the unique constraint.
	 */
	protected $_uniqueError = false;
	//
	// Public methods.
	/**
	 * This method is the one in charge to perform any needed action to verify
	 * if the represented database index is as it's specified by the XML file.
	 *
	 * @param multitype $info This object will contain entries for each issue
	 * found while checking. It's contents and structure would be something
	 * like this:
	 * @code
	 *	array(
	 *	    "errors"   => array(),
	 *	    "warnings" => array(),
	 *	    "not.in"   => array(
	 *	        "index"  => array(),
	 *	        "xml"    => array()
	 *	    )
	 *	)
	 * @endcode
	 * @return boolean Returns true when it is as it has to be.
	 */
	public function check(&$info = null) {
		//
		// Setting a default value to be returned.
		$ok = true;
		//
		// Creating a list for all the results. At the end, it will hold
		// the value for $info.
		$results = array();
		//
		// Setting the unique issue indicator as false.
		$this->_uniqueError = false;
		//
		// Checking that the specification is successfully loaded.
		if($this->loaded()) {
			//
			// Creating a statement to get all the required index
			// information.
			// @{
			$stmt = null;
			$stmtId = __CLASS__."::".__FUNCTION__;
			if($this->_db->has($stmtId)) {
				$stmt = $this->_db->get($stmtId);
			} else {
				global $wcDatabaseAccess;

				$query = "select  table_catalog,\n";
				$query .= "        table_schema,\n";
				$query .= "        table_name,\n";
				$query .= "        non_unique,\n";
				$query .= "        index_schema,\n";
				$query .= "        index_name,\n";
				$query .= "        seq_in_index,\n";
				$query .= "        column_name,\n";
				$query .= "        collation,\n";
				$query .= "        cardinality,\n";
				$query .= "        sub_part,\n";
				$query .= "        packed,\n";
				$query .= "        nullable,\n";
				$query .= "        index_type,\n";
				//$query .= "        index_comment,\n";
				$query .= "        comment\n";
				$query .= "from    information_schema.statistics\n";
				$query .= "where   table_schema = '{$wcDatabaseAccess["database"]}'\n";
				$query .= " and    index_schema      = table_schema\n";
				$query .= " and    lower(table_name) = lower(:table)\n";
				$query .= " and    lower(index_name) = lower(:index)\n";

				$stmt = $this->_db->prepare($stmtId, $query);
			}
			// @}
			//
			// Creating a list to hold index's columns specs.
			$index = array();
			//
			// Asking for the information.
			// @{
			$params = array(
				":index" => $this->_xml->name,
				":table" => "{$this->_dbprefix}{$this->_xml->tablename}"
			);
			if($stmt->execute($params)) {
				foreach($stmt->fetchAll() as $row) {
					//
					// Loading all index's columns taking care
					// of it position.
					$index[$row["seq_in_index"]] = $row;
				}
			}
			// @}
			//
			// Creating a basic structure for the information to
			// return (when $info is not null).
			$results["errors"] = array();
			$results["warnings"] = array();
			$results["not.in"] = array(
				"index" => array(),
				"xml" => array()
			);
			//
			// This variable indicate the position on the column being
			// analyzed inside the XML index specification.
			$xPosition = 1;
			//
			// Setting an ancillary flag for unicity.
			$xUnique = !isset($this->_xml->non_unique);
			//
			// Seeking over every specified column to check if it's
			// present in the database index and if its structure is
			// ok.
			foreach($this->_xml->columns->children() as $xColumn) {
				//
				// This flag indicates if there is a match between
				// XML and database index specifications.
				$found = false;
				//
				// This variable indicate the position on the
				// column being analyzed inside the database index
				// specification.
				$tPosition = 0;
				//
				// Seeking over every database index column to
				// check if its structure is correct.
				foreach($index as $column) {
					//
					// Index column position in database
					// specification is counted manually.
					$tPosition++;
					//
					// Based on the used query, each column
					// has the unique status for the index.
					$tUnique = !$column["non_unique"];
					//
					// Checking if the column's name matches.
					if("{$xColumn}" == $column["column_name"]) {
						//
						// At this point column is
						// supposed to be found.
						$found = true;
						//
						// Checking if column position
						// inside the index matches, if
						// not it is an error.
						if($xPosition != $tPosition) {
							//
							// Adding this event to the list of errors.
							$results["error"][] = "'{$this->name()}.{$xColumn}' order differs. x: '{$xPosition}'. t: '{$tPosition}'";
							$ok = false;
						}
						//
						// Checking if there is an issue
						// with the unique constraint.
						if(!$this->_uniqueError && $xUnique != $tUnique) {
							//
							// Adding this event to the list of errors.
							$results["error"][] = "'{$this->name()} unicity differs. x: '".($xUnique ? "true" : "false")."'. t: '".($tUnique ? "true" : "false")."'";
							$ok = false;
							//
							// Unique constraint issue flag is set to true.
							$this->_uniqueError = true;
						}
						//
						// Since a match was found, this
						// loop must stop.
						break;
					}
				}
				//
				// When all index columns from database where
				// checked and no one matched, this name must be
				// added to the proper list.
				if(!$found) {
					$results["not.in"]["index"][] = "{$xColumn}";
					$ok = false;
				}
				//
				// Current XML column position must jump to the
				// next.
				$xPosition++;
			}
			//
			// Searching for any index column specified in the
			// database that doesn't match with XML specifications.
			foreach($index as $column) {
				//
				// This flag indicates if there is a match between
				// XML and database index specifications.
				$found = false;
				//
				// Seeking over every index column specified by
				// XML file.
				foreach($this->_xml->columns->children() as $xColumn) {
					//
					// If names match, it is noted and this
					// loop ends.
					if("{$xColumn}" == $column["column_name"]) {
						$found = true;
						break;
					}
				}
				//
				// Checking that a match was found, otherwise, it
				// is an error and it must be informed in the
				// proper list.
				if(!$found) {
					$results["not.in"]["xml"][] = $column["column_name"];
					$ok = false;
				}
			}
		}
		//
		// If it was specified an information pointer, it is replaced with
		// collected results.
		if($info !== null) {
			$info = $results;
		}
		//
		// Returning check status.
		return $ok;
	}
	/**
	 * This method searches into all database index of current schema looking
	 * for one with name specified by the XML file.
	 *
	 * @return boolean Returns true when the index is created.
	 */
	public function checkExistence() {
		//
		// Setting a default value to be returned.
		$ok = false;
		//
		// Checking if a XML file was successfully loaded.
		if($this->loaded()) {
			//
			// Creating an statement to ask for the index.
			// @{
			$stmt = null;
			$stmtId = __CLASS__."::".__FUNCTION__;
			if($this->_db->has($stmtId)) {
				$stmt = $this->_db->get($stmtId);
			} else {
				global $wcDatabaseAccess;

				$query = "select  *\n";
				$query .= "from    information_schema.statistics\n";
				$query .= "where   table_schema      = '{$wcDatabaseAccess["database"]}'\n";
				$query .= " and    index_schema      = table_schema\n";
				$query .= " and    lower(table_name) = lower(:table)\n";
				$query .= " and    lower(index_name) = lower(:index)\n";

				$stmt = $this->_db->prepare($stmtId, $query);
			}
			// @}
			//
			// Asking for the index.
			// @{
			$params = array(
				":index" => $this->_xml->name,
				":table" => "{$this->_dbprefix}{$this->_xml->tablename}"
			);
			if($stmt->execute($params) && $stmt->rowCount() > 0) {
				//
				// When the statement returns rows, it means it
				// exists.
				$ok = true;
			}
			// @}
			//
			// Setting the internal flag for further existence checks.
			$this->_exists = $ok;
		}
		//
		// Returning check results.
		return $ok;
	}
	/**
	 * This method returns a list of queries that can be used to alter the
	 * database and leave it as it is specified on XML files.
	 *
	 * @return string[] Returns a list of queries to adapt the database. When
	 * it's not yet loaded, it returns false.
	 */
	public function ddlQueries() {
		//
		// Setting a default value to be returned.
		$queries = false;
		//
		// Checking the object is loaded.
		if($this->loaded()) {
			//
			// Creating a list to hold queries.
			$queries = array();
			//
			// If the index doesn't exist, it must be created. But if
			// it does exist and this method was called, it means
			// something is wrong and it must be recreated, in other
			// words, dropped and created.
			if(!$this->exists()) {
				$queries[] = $this->createQuery();
			} else {
				$queries[] = $this->dropQuery();
				$queries[] = $this->createQuery();
			}
		}
		//
		// Returning the list of queries.
		return $queries;
	}
	/**
	 * This method allows to load this object with a XML file.
	 *
	 * @param string $filename Full path relative to wcomix root directory.
	 * @return boolean Returns true when the XML file was loaded successfully.
	 */
	public function load($filename) {
		//
		// Calling parent class loader.
		if(parent::load($filename)) {
			//
			// Object name will be something like:
			//	'table_name[index_name]'
			$this->_name = "{$this->_dbprefix}{$this->_xml->tablename}[{$this->_xml->name}]";
		}
		//
		// Returning loading status.
		return $this->_loaded;
	}
	//
	// Protected methods.
	/**
	 * This method generates an appropriate query to create the represented
	 * database index.
	 *
	 * @return string Returns a query string.
	 */
	protected function createQuery() {
		//
		// Setting a default value to be returned.
		$query = "";
		//
		// Creating the list of columns used by the index to create.
		$columns = array();
		foreach($this->_xml->columns->children() as $xColumn) {
			$columns[] = "{$xColumn}";
		}
		//
		// Creating the generic part of the query.
		$query = "alter table {$this->_dbprefix}{$this->_xml->tablename}\n";
		//
		// In MySQL it is different to create a primary key, a unique
		// index or a simple index.
		if("{$this->_xml->name}" == "PRIMARY") {
			$query .= "        add primary key";
		} elseif(isset($this->_xml->non_unique)) {
			$query .= "        add index {$this->_xml->name}";
		} else {
			$query .= "        add unique index {$this->_xml->name}";
		}
		//
		// Appending the list of columns involved.
		$query .= "(".implode(",", $columns).")\n";
		//
		// Returning the generated query.
		return $query;
	}
	/**
	 * This method generates an appropriate query to drop the represented
	 * database index.
	 *
	 * @return string Returns a query string.
	 */
	protected function dropQuery() {
		//
		// Setting a default value to be returned.
		$query = "";
		//
		// Creating the generic part of the query.
		$query = "alter table {$this->_dbprefix}{$this->_xml->tablename}\n";
		//
		// In MySQL it is different to drop a primary key than a simple
		// index.
		if("{$this->_xml->name}" == "PRIMARY") {
			$query .= "        drop primary key";
		} else {
			$query .= "        drop index {$this->_xml->name}\n";
		}
		//
		// Returning the generated query.
		return $query;
	}
}
?>
